/*
 * Copyright 2011 Angel Sanadinov
 *
 * This file is part of VBox WMI.
 *
 * VBox WMI is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * VBox WMI is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with VBox WMI.  If not, see <http://www.gnu.org/licenses/>.
 */

package Managers;

import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import Beans.LogBean;
import Utilities.ConfigurationReader;
import java.util.ArrayList;
import java.util.GregorianCalendar;
import Utilities.Constants;
import Utilities.Constants.SystemLoggingLevel;
import Utilities.Interfaces.Controllable;
import Utilities.Interfaces.SelfValidating;
import javax.servlet.ServletContext;

/**
 * Manager for the logs that the system and its users generate. <br><br>
 * Logs are stored in two locations: a log file on the server and in the database.
 * Generally, the log file is used for system events only (system startup/shutdown,
 * exceptions, etc.) and the database logging is used for user generated events
 * (user logs in, user starts a virtual machine, etc.).
 * <br><br>
 * <b>Note:</b> <i>This manager is designed to work as a separate thread and must
 * always me stopped using the <b>stop()</b> method!</i>
 *
 * @author Angel Sanadinov
 */
public class LogsManager implements Runnable, SelfValidating, Controllable
{
    private DatabaseManager database;            //database manager for the regular logs
    private OutputStreamWriter logFile;          //log file for the system logs
    private ArrayList<String> pendingSystemLogs; //list of system logs that need to be written to file
    private boolean isValid = false;             //set to true when this manager is successfully constructed
    private boolean isWaiting = false;            //state variable; the manager is waiting for new logs
    private boolean isRunning = true;              //state variable; the manager is running
    private SystemLoggingLevel level;

    /**
     * Constructs the manager by creating the system logs list and opening the
     * system logs file.
     *
     * @param configuration configuration parameters for the manager
     * @param context servlet context (for parsing the logs file path)
     */
    public LogsManager(ConfigurationReader configuration, ServletContext context)
    {
        int pendingLogsListCapacity = 0;
        try
        {
            pendingLogsListCapacity = Integer.parseInt(
                    configuration.getParameter(ConfigurationReader.PARAM_PENDING_LOGS_LIST_CAPACITY));

            level = SystemLoggingLevel.valueOf(
                    configuration.getParameter(ConfigurationReader.PARAM_SYSTEM_LOGGING_LEVEL).toUpperCase());
        }
        catch(NumberFormatException e){}
        catch(IllegalArgumentException e)
        {
            level = SystemLoggingLevel.DEFAULT;
        }

        

        //checks for a sane value for the initial size of the list
        if(pendingLogsListCapacity > 0)
            pendingSystemLogs = new ArrayList<String>(pendingLogsListCapacity);
        else //negative or zero initial capacity; use default array list constructor
            pendingSystemLogs = new ArrayList<String>();

        try
        {
            String logsFilePath = context.getRealPath(configuration.getParameter(ConfigurationReader.PARAM_LOGS_FILE));
            String defaultCharset = configuration.getParameter(ConfigurationReader.PARAM_DEFAULT_CHARSET);
            //opens the file for writing (append) using the specified charset
            logFile = new OutputStreamWriter(new FileOutputStream(new File(logsFilePath), true), defaultCharset);
            //attempts to write to the file
            writeToFile(getEventString(System.currentTimeMillis(),
                    Constants.LOG_SEVERITY_INFORMATION, "Logs Manager started ("+level+")"));
            isValid = true; //the logs manager is now initialized and its thread can be started
        }
        catch(FileNotFoundException e)
        {isValid = false;}
        catch(UnsupportedEncodingException e)
        {isValid = false;}
        catch(IOException e)
        {isValid = false;}

        if(pendingLogsListCapacity <= 0)
        {
            writeToFile(getEventString(System.currentTimeMillis(), Constants.LOG_SEVERITY_WARNING, 
                                       "Logs Manager: Failed to parse initialization parameter. "
                                       + " Default value (" + pendingLogsListCapacity
                                       + ") for pendingLogsCapacity was used."));
        }
        else
            ;
    }

    /**
     * The main execution method. <br><br>
     * It is used for processing logs that need to be written to the system logs
     * file.
     * <br><br>
     * <b>Note:</b> <i>The thread should always be stopped using the <b>stop()</b> method!</i>
     * @see LogsManager Logs Manager
     */
    @Override
    public void run()
    {
        //checks if the manager is flagged for stopping and if there are more logs to be written
        while(isRunning || !pendingSystemLogs.isEmpty())
        {
            //while there are still logs on the list, writes the to the logs file
            while(!pendingSystemLogs.isEmpty())
                writeToFile(pendingSystemLogs.remove(0));

            //no more work to do; sets the proper thread state
            isWaiting = true;

            //checks if the manager is flagged for stopping and suspends the current thread
            if(isRunning)
                sleepLogger();
            else
                ;

            //there is work to do or the manager is to be stopped, ie: it's not waiting
            isWaiting = false;
        }

        //logs the manager stopping event
        writeToFile(getEventString(System.currentTimeMillis(), Constants.LOG_SEVERITY_INFORMATION, "Logs Manager stopping..."));

        pendingSystemLogs.clear(); //clears any remaining unprocessed logs
        pendingSystemLogs = null;  //removes the reference to the list
        database = null;           //removes the reference to the database manager
        level = null;
        
        try
        {
            logFile.close();    //flushes and closes the logs file stream
        }
        catch(IOException e)
        {
            //and do what...?
        }
        finally
        {
            logFile = null; //removes the reference to the logs file
        }
    }

    /**
     * Flags the manager for stopping.
     *
     * @see LogsManager Logs Manager
     * @see LogsManager#run()
     */
    @Override
    public void stop()
    {
        isRunning = false;  //sets the flag
        notifyLogger();     //notifies the thread of the event
    }

    /**
     * Sets the reference to the database manager once the logs and database managers
     * have been initialized. <br><br>
     * <b>Note:</b> <i>The logs manager needs to be started first, then the database
     * manager and then this method should be used to set the database manager reference.</i>
     *
     * @param manager the reference to the initialized database manager
     * @see DatabaseManager Database Manager
     */
    public void setDatabaseManagerReference(DatabaseManager manager)
    {
        if(manager != null)
            this.database = manager;
        else
            ;
    }

    /**
     * Logs an event in the database (when server id is not available). <br><br>
     * <b>Note:</b> <i>The database manager reference should be set using the
     * appropriate method before attempting to use this one.</i>
     *
     * @param eventTime the time when the event occurred, in milliseconds
     * @param severity the severity of the event
     * @param logMessage the message to be logged
     * @param initiatorId the id of the user that initiated the event/error
     * 
     * @see System#currentTimeMillis() 
     * @see Constants Severity Constants
     */
    public void log(long eventTime, String severity, String logMessage, int initiatorId)
    {
        log(eventTime, severity, logMessage, initiatorId, 0);
    }

    /**
     * Logs an event in the database (when server id is available). <br><br>
     * <b>Note:</b> <i>The database manager reference should be set using the
     * appropriate method before attempting to use this one.</i>
     *
     * @param eventTime the time when the event occurred, in milliseconds
     * @param severity the severity of the event
     * @param logMessage the message to be logged
     * @param initiatorId the id of the user that initiated the event/error
     * @param serverId the id of the server on which the event took place
     *
     * @see System#currentTimeMillis()
     * @see Constants Severity Constants
     */
    public void log(long eventTime, String severity, String logMessage, int initiatorId, int serverId)
    {
        //checks the database reference and logs the event
        if(database != null)
            database.insertLog(new LogBean(initiatorId, serverId, severity, eventTime, logMessage));
        else
            ;
    }

    /**
     * Logs an event in the system logs file. <br><br>
     * <b>Note:</b> <i>The event will not be logged if the manager is flagged
     * for stopping.</i>
     *
     * @param eventTime the time when the event occurred, in milliseconds
     * @param severity the severity of the event
     * @param logMessage the message to be logged
     *
     * @see System#currentTimeMillis()
     * @see Constants Severity Constants
     */
    final public void logSystemEvent(long eventTime, String severity, String logMessage)
    {
        //checks the status of the manager, the logging level and 
        //whether a debugging message is allowed to be logged
        if(isRunning && level != SystemLoggingLevel.NONE
                && (!severity.equals(Constants.LOG_SEVERITY_DEBUG) || level == SystemLoggingLevel.DEBUG))
        {
            //adds the event to the list and notifies the thread
            pendingSystemLogs.add(getEventString(eventTime, severity, logMessage));
            this.notifyLogger();
        }
        else
            ;
    }

    /**
     * Checks the validity of the manager. <br><br>
     * <b>Note:</b> <i>The manager is considered valid when it has been successfully
     * constructed. The state of its thread (if started) is not checked for validity.</i>
     *
     * @return <code>true</code> if the manager is valid
     */
    @Override
    public boolean isValid()
    {
        return isValid;
    }

    /**
     * Builds the string that will be written to the system logs file.
     *
     * @param eventTime the time when the event occurred, in milliseconds
     * @param severity the severity of the event
     * @param logMessage the message to be logged
     * 
     * @return the formatted log String
     *
     * @see System#currentTimeMillis()
     * @see Constants Severity Constants
     */
    private String getEventString(long eventTime, String severity, String logMessage)
    {
        StringBuilder eventString = new StringBuilder();        //buffer for more efficient string processing
        GregorianCalendar eventDate = new GregorianCalendar();  //a calendar for the event time/date
        eventDate.setTimeInMillis(eventTime);                   //sets the proper time in the calendar

        //builds the event string
        eventString.append(eventDate.get(GregorianCalendar.YEAR));
        eventString.append("-");
        eventString.append(String.format("%02d", eventDate.get(GregorianCalendar.MONTH) + 1));
        eventString.append("-");
        eventString.append(String.format("%02d", eventDate.get(GregorianCalendar.DAY_OF_MONTH)));
        eventString.append(" ");
        eventString.append(String.format("%02d", eventDate.get(GregorianCalendar.HOUR_OF_DAY)));
        eventString.append(":");
        eventString.append(String.format("%02d", eventDate.get(GregorianCalendar.MINUTE)));
        eventString.append(":");
        eventString.append(String.format("%02d", eventDate.get(GregorianCalendar.SECOND)));
        eventString.append(", ");
        eventString.append(severity);
        eventString.append(", ");
        eventString.append(logMessage);
        eventString.append("\r\n");

        return eventString.toString();
    }

    /**
     * Suspends the manager thread.
     */
    private synchronized void sleepLogger()
    {
        try
        {
            wait();
        }
        catch(InterruptedException e)
        {/*and do what...?*/}
    }

    /**
     * Awakes the manager thread if it is suspended.
     */
    private synchronized void notifyLogger()
    {
        if(isWaiting)
            notify();
        else
            ;
    }

    /**
     * Writes the supplied message to the system logs file.
     *
     * @param message the log message to be written
     */
    private void writeToFile(String message)
    {
        try
        {
            logFile.write(message);
            logFile.flush();
        }
        catch(IOException e)
        {
            //and do what...?
        }
    }
}
